﻿namespace SpikingVisualisationRLib

open SpikingAnalyticsLib
open RProvider
open RProvider.RInterop
open RProvider.``base``
open RProvider.graphics
open RProvider.grDevices

/// Plot high temporal resolution data
[<AbstractClass; Sealed>]
type MillisecondResolutionDataVisualisation () =

    static let SetParameters data =
        namedParams data
        |> R.par |> ignore

    static let DrawPlot data =
        namedParams data
        |> R.plot |> ignore

    static let DrawAxis data =
        namedParams data
        |> R.axis |> ignore
        
    static let DrawLabel data =
        namedParams data
        |> R.mtext |> ignore

    static let DrawBox data =
        namedParams data
        |> R.box |> ignore

    static let DrawLegend data =
        namedParams data
        |> R.legend |> ignore

    static let ShowHiResDataPlot startMillisecond (y1:double []) (y2:double [] option) (yLabel1:string) (yLabel2:string option) (y1Clip:double option) (y2Clip:double option) (y1Limit:(int list) option) (y2Limit:(int list) option) =

        let GetNArray n (clip:double option) data =
            data
            |> Seq.take n
            |> Seq.map (fun element -> if clip.IsNone || element <= clip.Value then element else clip.Value)
            |> Seq.toArray

        let numDataPoints =
            if y2.IsSome then
                let l1, l2 = y1.Length, y2.Value.Length
                if l1 < l2 then l1 else l2
            else
                y1.Length

        let y1Clipped = GetNArray numDataPoints y1Clip y1
        let y2Clipped = if y2.IsSome then Some(GetNArray numDataPoints y2Clip y2.Value) else None
        let x = Array.init numDataPoints (fun index -> index + startMillisecond)

        // set the source for calculating the y-axis range (either the y-axis limit (if set) or the original y-axis data)
        let GetRangeData (limit:int list option) data =
            if limit.IsSome then
                limit.Value
                |> Seq.map (fun v -> float v)
                |> Seq.toArray
            else
                data
        let y1RangeData = GetRangeData y1Limit y1Clipped
        let y2RangeData = if y2Clipped.IsSome then Some(GetRangeData y2Limit y2Clipped.Value) else None

        // add extra space to right margin of plot within frame
        SetParameters [ "mar", box [ 4.1; 4.1; 2.1; 4.1; ]; ]   // bottom, left, top, right

        // draw the first plot
        DrawPlot [
            "x", box x;
            "y", box y1Clipped;
            "type", box "l";
            "lty", box 1;
            "lwd", box 1;
            "col", box "black";
            "xlab", box "";
            "ylab", box "";
            "ylim", (if y1Limit.IsSome then box y1Limit.Value else box None);
            "axes", box false;
        ]

        // draw the left axis (las=1 makes horizontal labels)
        DrawAxis [
            "side", box 2;
            "at", box (R.pretty(R.range(y1RangeData)));
            "col", box "black";
            "las", box 1;
            //"ylim", box [ 0; 1; ];
        ]

        // draw the left axis label
        DrawLabel [
            "text", box yLabel1;
            "side", box 2;
            "line", box 3.0;
            "las", box 1;
        ]

        // allow a new plot to the same device
        SetParameters [ "new", box true ]

        // draw the second plot
        if y2Clipped.IsSome then
            DrawPlot [
                "x", box x;
                "y", box y2Clipped.Value;
                "type", box "l";
                "lty", box 2;
                "lwd", box 1;
                "axes", box false;
                "bty", box "n";
                "col", box "gray";
                "xlab", box "";
                "ylab", box "";
                "ylim", (if y2Limit.IsSome then box y2Limit.Value else box None);
            ]

            // draw the right axis
            DrawAxis [
                "side", box 4;
                "at", box (R.pretty(R.range(y2RangeData.Value)));
                "col", box "gray";
                "col.axis", box "gray";
                "las", box 1;
                //"ylim", box [ 0; 1; ];
            ]

            // draw the right axis label
            DrawLabel [
                "text", box yLabel2.Value;
                "side", box 4;
                "line", box 3.0;
                "col", box "gray";
                "las", box 1;
            ]

        // draw the x-axis
        DrawAxis [
            "side", box 1;
            "at", box (R.pretty(R.range(x)));
        ]

        // draw the x-axis label
        DrawLabel [
            "text", box "Time (msec)";
            "side", box 1;
            "col", box "black";
            "line", box 2.5;
        ]

        // draw a box around the plot area (writes over the gray axis line but leaves gray tick marks)
        DrawBox [
            "bty", box "o";
        ]

    //    // draw the legend
    //    DrawLegend [
    //        "x", box "topleft";
    //        "inset", box 0.05;
    //        "pch", box 16;
    //        "legend", box [ yLabel1; yLabel2; ];
    //        "col", box [ "black"; "gray"; ];
    //        "text.col", box [ "black"; "gray"; ];
    //    ]

    static let SelectData startMillisecond endMillisecond (data:(#seq<double> * #seq<double>)) =
        let Select data =
            data
            |> Seq.skip startMillisecond
            |> Seq.take ((endMillisecond - startMillisecond) + 1)
            |> Seq.toArray

        let y1, y2 = data
        Select y1, Select y2

    // Each row is the time-series data for a given neuron or connection
    static let ShowCollectedData startMillisecond y1Label y2Label (y1Clip:double option) (y2Clip:double option) (y1Limit:(int list) option) (y2Limit:(int list) option) numberOfRows (dataSelector:#seq<double> * #seq<double> -> double [] * double []) data =

        // set the margins and number of rows
        namedParams [
            "mar", box [ 2; 4; 1; 4; ];
            "mfrow", box [numberOfRows; 1];
        ]
        |> R.par |> ignore

        data
        |> Seq.iter (fun (rowData) ->
            // select the required block of time for the current row (i.e. neuron or connection)
            let y1, y2 = dataSelector rowData
            ShowHiResDataPlot startMillisecond y1 (Some(y2)) y1Label y2Label y1Clip y2Clip y1Limit y2Limit
        )

    /// Plot high temporal resolution data such as membrane or synaptic derivative data
    static member ShowHiResolutionDataPlot(y1:#seq<double>, y2:#seq<double> option, yLabel1:string, yLabel2:string option) =

        let y2Data = if y2.IsSome then Some(Seq.toArray y2.Value) else None

        ShowHiResDataPlot 0 (Seq.toArray y1) (y2Data) yLabel1 yLabel2 None None None None

    /// Plot high temporal resolution data such as membrane or synaptic derivative data
    static member ShowHiResolutionDataPlot(y1:#seq<double>, y2:#seq<double> option, yLabel1:string, yLabel2:string option, y1Clip:double option, y2Clip:double option, y1Limit:(int list) option, y2Limit:(int list) option) =

        let y2Data = if y2.IsSome then Some(Seq.toArray y2.Value) else None

        ShowHiResDataPlot 0 (Seq.toArray y1) (y2Data) yLabel1 yLabel2 y1Clip y2Clip y1Limit y2Limit

    /// Show groups of high temporal resolution data as multiple rows of plots
    // see SelectMembraneGroups() and SelectWeightGroups() member functions of OneMillisecTickDataCollector type for suitable sources for the 'groups' parameter
    static member ShowHiResolutionDataPlotGroups(groups, ?startMillisecond:int, ?endMillisecond:int, ?yLabel1:string, ?yLabel2:string, ?y1Clip:double, ?y2Clip:double, ?y1Limit:int list, ?y2Limit:int list) =

        let groupList = Seq.toList groups

        // set the margins and number of rows
        namedParams [
            "mar", box [ 2; 4; 1; 4; ];
            "mfrow", box [groupList.Length; 1];
        ]
        |> R.par |> ignore

        let baseIndex = if startMillisecond.IsSome then startMillisecond.Value else 0

        let label1 = if yLabel1.IsSome then yLabel1.Value else "Y1"
        let label2 = if yLabel2.IsSome then yLabel2.Value else "Y2"

        groupList
        |> Seq.iter (fun (y1, y2) ->
            ShowHiResDataPlot baseIndex y1 y2 label1 (Some(label2)) y1Clip y2Clip y1Limit y2Limit
        )

    /// Show membrane data with a row for each neuron
    static member ShowCollectedMembraneData(hiResCollector:OneMillisecTickDataCollector) =

        let data = hiResCollector.MembraneDataRowView
        if data.IsSome then
            let y1Clip = Some(30.0)
            let y1Limit = Some([ -80; 40; ])   // below baseline to above max spike value
            ShowCollectedData 0 "v" (Some("u")) y1Clip None y1Limit None hiResCollector.NumberOfSampledNeurons (function data -> data) data.Value

    /// Show selected membrane data with a row for each neuron
    static member ShowCollectedMembraneData(hiResCollector:OneMillisecTickDataCollector, startMillisecond, endMillisecond) =

        let data = hiResCollector.MembraneDataRowView
        if data.IsSome then
            let y1Clip = Some(30.0)
            let y1Limit = Some([ -80; 40; ])   // below baseline to above max spike value

            let dataSelector = SelectData startMillisecond endMillisecond
            ShowCollectedData startMillisecond "v" (Some("u")) y1Clip None y1Limit None hiResCollector.NumberOfSampledNeurons dataSelector data.Value

    /// Show synaptic weight data with a row for each connection
    // TODO MG use the sampling times from the store for the x-axis
    static member ShowCollectedWeightData(hiResCollector:OneMillisecTickDataCollector) =

        let data = hiResCollector.ConnectionDataRowView
        if data.IsSome then
            ShowCollectedData 0 "w" (Some("d")) None None None None hiResCollector.NumberOfSampledConnections (function data -> data) data.Value

    /// Show selected synaptic weight data with a row for each connection
    static member ShowCollectedWeightData(hiResCollector:OneMillisecTickDataCollector, startMillisecond, endMillisecond) =

        let data = hiResCollector.ConnectionDataRowView
        if data.IsSome then
            let dataSelector = SelectData startMillisecond endMillisecond
            ShowCollectedData startMillisecond "w" (Some("d")) None None None None hiResCollector.NumberOfSampledConnections dataSelector data.Value
